using System;
using System.IO;
using System.Reflection;
using Unity.Collections;
using Unity.Entities;
using Unity.Scenes.Editor;
using UnityEditor.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
using Unity.Entities.Hybrid.Baking;
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
using Hash128 = Unity.Entities.Hash128;
using Object = UnityEngine.Object;

// ReSharper disable once CheckNamespace
public static class SerializeEditorTool
{
    private const string EntityExtension = "bytes";
    private const string ObjRefsExtension = "asset";
    
    [MenuItem("Assets/ECSTool/Serialize GameObjects", false)]
    public static void SerializeSelection_GameObject()
    {
        int count = Selection.gameObjects.Length;
        float index = 0.0f;
        foreach (var go in Selection.gameObjects)
        {
            EditorUtility.DisplayProgressBar(index + "/" + count, "", index / count);
            DoSerialize(go);
            EditorUtility.ClearProgressBar();
        }
    }
    
    [MenuItem("Assets/ECSTool/Serialize Scene", false)]
    public static void SerializeSelection_Scene()
    {
        string scenePath = AssetDatabase.GetAssetPath(Selection.activeObject);
        if (!scenePath.EndsWith(".unity"))
        {
            return;
        }
        if (!SerializeScene(scenePath))
        {
            return;
        }
        
        AssetDatabase.Refresh();
    }

    private static void DoSerialize(GameObject go)
    {
        if (!Serialize(go))
        {
            return;
        }
        AssetDatabase.Refresh();
    }

    private static Type bakingUtilType;
    private static object bakingSettings;
    private static void BakeGameObjects(World world, GameObject[] insts)
    {
        if (bakingUtilType == null)
        {
            Assembly assembly = typeof(IBaker).Assembly;
            bakingUtilType = assembly.GetType("Unity.Entities.BakingUtility");
        }
        if (bakingUtilType == null)
        {
            Debug.LogError("[SerializeEditorTool.BakeGameObjects] Could not find type of BakingUtility");
            return;
        }
        if (bakingSettings == null)
        {
            Assembly assembly = typeof(IBaker).Assembly;
            Type bakingSettingsType = assembly.GetType("Unity.Entities.BakingSettings");
            if (bakingSettingsType == null)
            {
                Debug.LogError("[SerializeEditorTool.BakeGameObjects] Could not find type of BakingSettings");
                return;
            }
            bakingSettings = Activator.CreateInstance(bakingSettingsType);
            const uint flag = 1 << 2 | 1 << 0;
            // settings.BakingFlags = BakingUtility.BakingFlags.AssignName | BakingUtility.BakingFlags.AddEntityGUID
            FieldInfo fieldInfo =
                bakingSettingsType.GetField("BakingFlags", BindingFlags.Instance | BindingFlags.Public);
            if (fieldInfo == null)
            {
                Debug.LogError(
                    "[SerializeEditorTool.BakeGameObjects] Could not find field BakingFlags in BakingSettings");
                return;
            }
            fieldInfo.SetValue(bakingSettings, flag);
        }
        
        MethodInfo methodInfo =
            bakingUtilType.GetMethod("BakeGameObjects", BindingFlags.Static | BindingFlags.NonPublic);
        if (methodInfo == null)
        {
            Debug.LogError(
                "[SerializeEditorTool.BakeGameObjects] Could not find method BakeGameObjects in BakingUtility");
            return;
        }
        methodInfo.Invoke(null, new[] { world, insts, bakingSettings });
    }
    
    private static object sceneBakingSettings;
    private static readonly int useLightmapID = Shader.PropertyToID("_UseLightmap");
    private static readonly int receiveShadowOffID = Shader.PropertyToID("_ReceiveShadowOff");

    private static void BakeScene(World world, Scene scene)
    {
        if (bakingUtilType == null)
        {
            Assembly assembly = typeof(IBaker).Assembly;
            bakingUtilType = assembly.GetType("Unity.Entities.BakingUtility");
        }
        if (bakingUtilType == null)
        {
            Debug.LogError("[SerializeEditorTool.BakeScene] Could not find type of BakingUtility");
            return;
        }
        if (sceneBakingSettings == null)
        {
            Assembly assembly = typeof(IBaker).Assembly;
            Type bakingSettingsType = assembly.GetType("Unity.Entities.BakingSettings");
            if (bakingSettingsType == null)
            {
                Debug.LogError("[SerializeEditorTool.BakeGameObjects] Could not find type of BakingSettings");
                return;
            }
            sceneBakingSettings = Activator.CreateInstance(bakingSettingsType);
            const uint flag = 1 << 0 | 1 << 2 | 1 << 4;
            // settings.BakingFlags = BakingUtility.BakingFlags.AssignName | BakingUtility.BakingFlags.AddEntityGUID
            FieldInfo flagFieldInfo =
                bakingSettingsType.GetField("BakingFlags", BindingFlags.Instance | BindingFlags.Public);
            if (flagFieldInfo == null)
            {
                Debug.LogError(
                    "[SerializeEditorTool.BakeScene] Could not find field BakingFlags in BakingSettings");
                return;
            }
            flagFieldInfo.SetValue(sceneBakingSettings, flag);
            
            FieldInfo guidFieldInfo =
                bakingSettingsType.GetField("SceneGUID", BindingFlags.Instance | BindingFlags.Public);
            if (guidFieldInfo == null)
            {
                Debug.LogError(
                    "[SerializeEditorTool.BakeScene] Could not find field SceneGUID in BakingSettings");
                return;
            }
            guidFieldInfo.SetValue(sceneBakingSettings, (Hash128)AssetDatabase.GUIDFromAssetPath(scene.path));
        }
        
        MethodInfo methodInfo =
            bakingUtilType.GetMethod("BakeScene", BindingFlags.Static | BindingFlags.NonPublic);
        if (methodInfo == null)
        {
            Debug.LogError(
                "[SerializeEditorTool.BakeScene] Could not find method BakeScene in BakingUtility");
            return;
        }
        methodInfo.Invoke(null, new[] { world, scene, sceneBakingSettings, false, null });
    }

    private static bool Serialize(GameObject prefab)
    {
        string assetPath = AssetDatabase.GetAssetPath(prefab);

        var serializeWorld = new World("serialize World");
        var serializeWorldEntityMgr = serializeWorld.EntityManager;
        try
        {
            GameObject prefabInst = Object.Instantiate(prefab);
            prefabInst.name = prefab.name;

            prefabInst.AddComponent<LinkedEntityGroupAuthoring>();

            // BakingUtility.BakeGameObjects(serializeWorld, new[] {prefabInst});
            
            BakeGameObjects(serializeWorld, new[] {prefabInst});
            var query = serializeWorldEntityMgr.CreateEntityQuery(new EntityQueryDesc
            {
                All = new[]
                {
                    new ComponentType(typeof(AssetComponents))
                }
            });
            var entities = query.ToEntityArray(Allocator.Temp);
            Debug.Log("baked entities.Length:"+entities.Length);
            
            Serialize(serializeWorldEntityMgr, assetPath);
            
            Object.DestroyImmediate(prefabInst, true);
            
            entities.Dispose();
            serializeWorld.Dispose();
        }
        catch (Exception ex)
        {
            Debug.LogError($"Convert {assetPath} to entity failed:\n{ex}");
            return false;
        }

        return true;
    }

    private static bool SerializeScene(string scenePath)
    {
        Scene scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
        
        var serializeWorld = new World("serialize World");
        var serializeWorldEntityMgr = serializeWorld.EntityManager;

        try
        {
            SceneManager.SetActiveScene(scene);

            BakeScene(serializeWorld, scene);
            
            Serialize(serializeWorldEntityMgr, scenePath);
            
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            
            string binaryPath = Path.ChangeExtension(scenePath, ObjRefsExtension);
            string fileName = Path.GetFileNameWithoutExtension(binaryPath);
            string directoryPath = binaryPath
                .Substring(0, binaryPath.Length - fileName.Length - ObjRefsExtension.Length - 1)
                .ToUNIXStyle();
            directoryPath += "SceneMaterials/";
            if (!Directory.Exists(directoryPath))
            {
                Directory.CreateDirectory(directoryPath);
            }
            
            var objRefs = AssetDatabase.LoadAssetAtPath<Unity.Scenes.ReferencedUnityObjects>(binaryPath);
            for (int i = 0; i < objRefs.Array.Length; ++i)
            {
                Object objRef = objRefs.Array[i];
                if (objRef is not Material mat)
                {
                    continue;
                }
                if (!mat.name.EndsWith("Lightmapped_"))
                {
                    continue;
                }
                if (!mat.IsKeywordEnabled("LIGHTMAP_ON"))
                {
                    continue;
                }
                Material matInst = Object.Instantiate(mat);
                matInst.name = mat.name;
                matInst.EnableKeyword("_LIGHTMAP_ON");
                matInst.DisableKeyword("LIGHTMAP_ON");
                if (matInst.HasProperty(useLightmapID))
                {
                    matInst.SetFloat(useLightmapID, 1.0f);
                }
                if (matInst.HasProperty(receiveShadowOffID))
                {
                    matInst.SetFloat(receiveShadowOffID, 1.0f);
                }
                string matPath = directoryPath + fileName + "_" + mat.name + ".mat";
                matPath = matPath.Replace("Lightmapped_", "Lightmapped");
                AssetDatabase.DeleteAsset(matPath);
                AssetDatabase.CreateAsset(matInst, matPath);

                objRefs.Array[i] = matInst;
                EditorUtility.SetDirty(objRefs);
            }
            AssetDatabase.Refresh();
        }
        catch (Exception ex)
        {
            Debug.LogError($"Convert {scenePath} to entity failed:\n{ex}");
            return false;
        }
        finally
        {
            serializeWorld.Dispose();
            
            EditorSceneManager.CloseScene(scene, true);
        }
        return true;
    }
    
    private static void EditorEntityScenesWrite(EntityManager entityManager, string binaryPath, string objectReferencesPath)
    {
        Type entitySceneType = typeof(EditorEntityScenes);
        MethodInfo writeMethod = entitySceneType.GetMethod("Write", BindingFlags.Static | BindingFlags.NonPublic);
        if (writeMethod == null)
        {
            return;
        }
        writeMethod.Invoke(null, new object[] { entityManager, binaryPath, objectReferencesPath });
    }

    private static void Serialize(EntityManager manager, string assetPath)
    {
        string binaryPath = Path.ChangeExtension(assetPath, EntityExtension);
        if (File.Exists(binaryPath))
        {
            AssetDatabase.DeleteAsset(binaryPath);
        }
        
        string objectReferencesPath = Path.ChangeExtension(assetPath, ObjRefsExtension);
        if (File.Exists(objectReferencesPath))
        {
            AssetDatabase.DeleteAsset(objectReferencesPath);
        }
        
        EditorEntityScenesWrite(manager, binaryPath, objectReferencesPath);
    }
}